package utils

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"time"

	"golang.org/x/net/publicsuffix"
)

const httpOnlyPrefix = "#HttpOnly_"

const CRC_VERIFY_KEY = "data-length"

// HTTPClient http请求对象
type HTTPClient struct {
	Client     *http.Client
	UA         string
	CookiePath string
	lock       sync.Mutex
}

// NewHTTPClient 初始化http对象
func NewHTTPClient() *HTTPClient {
	c := &HTTPClient{
		Client: &http.Client{},
	}
	CurCookieJar, err := cookiejar.New(nil)

	if err != nil {
		Error("NewHTTPClient Error : " + err.Error())
		panic(err)
	}
	c.Client.Jar = CurCookieJar

	c.UA = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"

	return c
}

func (c *HTTPClient) SetCookiePath(path string) error {
	c.CookiePath = path
	err := c.LoadCookieJarFile()
	if err != nil {
		c.CookiePath = ""
		return fmt.Errorf("NewHTTPClient LoadCookieJarFile Error : " + err.Error())
	}
	return nil
}

// SetCookies 手动设置cookie
func (c *HTTPClient) SetCookies(u *url.URL, cookies []*http.Cookie) {
	c.Client.Jar.SetCookies(u, cookies)
}

func (c *HTTPClient) Api(u string, data interface{}, key string) (ret string, code int, retHeaders *http.Header, err error) {
	encode, crc, err := GenApiData(data, key)
	if err != nil {
		return "", 50000, nil, err
	}
	return c.Post(u, map[string]string{
		"data": encode,
	}, map[string]string{
		CRC_VERIFY_KEY: crc,
	})
}

func GenApiData(data interface{}, key string) (string, string, error) {
	var aestype = GetRandType()
	encode, err := AesEncode(data, key, aestype)
	if err != nil {
		return "aes encode error", "", err
	}
	var crc = CRC32(encode)
	var strCrc = fmt.Sprintf("%v%d", crc, int(aestype))

	Info("aes encode : %s", encode)
	Info("crc : %v", crc)
	return ApiUrlEncode(encode), strCrc, nil
}

// Post Post数据
func (c *HTTPClient) Post(u string, data interface{}, headers map[string]string) (ret string, code int, retHeaders *http.Header, err error) {
	retdata, code, retHeaders, err := c.http(u, data, headers)
	if err != nil {
		Error(err.Error())
		return
	}
	ret = string(retdata)
	return
}

// Get Get数据
func (c *HTTPClient) Get(u string, headers map[string]string) (ret string, code int, retHeaders *http.Header, err error) {
	retdata, code, retHeaders, err := c.http(u, nil, headers)
	if err != nil {
		_ = Error(err.Error())
		return
	}
	ret = string(retdata)
	return
}

func (c *HTTPClient) http(u string, data interface{}, headers map[string]string) (ret []byte, code int, retHeaders *http.Header, err error) {
	Info("HTTP url: %s", u)

	var req *http.Request
	if nil == data {
		Info("Method : GET")
		req, err = http.NewRequest("GET", u, nil)
		if err != nil {
			return
		}
	} else {
		Info("Method : POST")
		Info("POST DATA : %+v", data)
		switch data.(type) {
		case string:
			postDataBytes := []byte(data.(string))
			postDataReader := bytes.NewReader(postDataBytes)
			req, err = http.NewRequest("POST", u, postDataReader)
			if err != nil {
				return
			}
		case map[string]string:
			postValues := url.Values{}
			for key, value := range data.(map[string]string) {
				postValues.Set(key, value)
			}
			postData := strings.Replace(postValues.Encode(), "+", "%20", -1)
			postDataBytes := []byte(postData)
			postDataReader := bytes.NewReader(postDataBytes)
			req, err = http.NewRequest("POST", u, postDataReader)
			if err != nil {
				return
			}
		default:
			err = Error("Post data type not accept! %+v", data)
			return
		}
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}

	if nil != headers {
		for key, value := range headers {
			req.Header.Set(key, value)
		}
	}
	if len(c.UA) > 0 {
		req.Header.Set("User-Agent", c.UA)
	}

	resp, err := c.Client.Do(req)
	if err != nil {
		_ = Error("http error : " + err.Error())
		return
	}
	defer resp.Body.Close()
	ret, err = ioutil.ReadAll(resp.Body)
	retHeaders = &resp.Header
	defer c.SaveCookieJarToFile(u, retHeaders, resp.Cookies())
	code = resp.StatusCode
	return
}

func splitKV(data string) (k, v string) {
	idx := strings.Index(data, "=")
	if -1 != idx {
		k = data[0:idx]
		v = data[idx+1:]
	} else {
		k = data
	}
	return
}

// download 下载数据（异步）

// 从浏览器获取cookie
// https://studygolang.com/articles/22575?fr=sidebar

// SaveCookieJarToFile 将Cookie保存到文件
// 转换方式 ： https://github.com/kosh04/cookiedisk/blob/master/entry.go
func (c *HTTPClient) SaveCookieJarToFile(u string, headers *http.Header, cookies []*http.Cookie) error {
	url, err := url.Parse(u)
	if err != nil {
		return err
	}
	c.lock.Lock()
	defer c.lock.Unlock()

	uDomain := url.Host

	file, err := GetFile(c.CookiePath)
	if err != nil {
		return err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)

	var lines []string

	for scanner.Scan() {
		line := scanner.Text()
		var cookieline = line
		if strings.HasPrefix(line, httpOnlyPrefix) {
			cookieline = strings.TrimPrefix(line, httpOnlyPrefix)
		}
		cookieFiellds := strings.Split(cookieline, "\t")
		if strings.Index(cookieFiellds[0], uDomain) == -1 && strings.Index(uDomain, cookieFiellds[0]) == -1 {
			lines = append(lines, line)
		}
	}

	mapCookies := make(map[string]*http.Cookie)
	if len(cookies) > 0 {
		for _, cookie := range cookies {
			mapCookies[cookie.Name] = cookie
		}
	}

	for _, line := range headers.Values("Set-Cookie") {
		cookie := c.ParseCookieFromLine(line)
		if nil != cookie {
			mapCookies[cookie.Name] = cookie
		}
	}

	for _, cookie := range mapCookies {
		row, err := parseCookieToLine(uDomain, cookie)
		if err != nil {
			return err
		}
		lines = append(lines, row)
	}

	file.Truncate(0)
	file.Seek(0, io.SeekStart)
	for _, line := range lines {
		_, err = Write(file, line+"\n")
		if err != nil {
			return err
		}
	}
	return nil
}

func (h *HTTPClient) ParseCookieFromLine(line string) *http.Cookie {
	cookieFields := strings.Split(line, "; ")
	var err error
	if len(cookieFields) > 0 {
		cookie := &http.Cookie{}
		cookie.Name, cookie.Value = splitKV(cookieFields[0])
		for i := 1; i < len(cookieFields); i++ {
			k, v := splitKV(cookieFields[i])
			switch strings.ToLower(k) {
			case "path":
				cookie.Path = v
			case "expires":
				cookie.Expires, err = time.Parse(http.TimeFormat, v)
				if err != nil {
					cookie.Expires, err = time.Parse("Mon, 02-Jan-06 15:04:05 GMT", v)
					if err != nil {
						t, err := StringToInt(v)
						if err != nil {
							Warning("expires Parse error : %s", err)
							cookie.Expires = time.Time{}
						} else if t == 0 {
							cookie.Expires = time.Time{}
						} else {
							cookie.Expires = time.Unix(int64(t/1000), 0)
						}
					}
				}
			case "max-age":
				i, err := strconv.Atoi(v)
				if err == nil {
					cookie.MaxAge = i
				} else {
					Warning("max-age strconv Atoi error : %s", err)
				}
			case "domain":
				cookie.Domain = v
			case "secure":
				cookie.Secure = true
			case "httponly":
				cookie.HttpOnly = true
			}
		}
		return cookie
	}
	return nil
}

// LoadCookieJarFile takes a path to a curl (netscape) cookie jar file and crates a go http.CookieJar with the contents
func (c *HTTPClient) LoadCookieJarFile() error {
	c.lock.Lock()
	defer c.lock.Unlock()
	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
	if err != nil {
		return err
	}
	file, err := GetFile(c.CookiePath)
	if err != nil {
		return err
	}
	defer file.Close()

	lineNum := 1
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		cookieLine := scanner.Text()
		cookie, err := parseCookieLineToCookie(cookieLine, lineNum)
		if cookie == nil {
			continue
		}
		if err != nil {
			return err
		}

		var cookieScheme string
		if cookie.Secure {
			cookieScheme = "https"
		} else {
			cookieScheme = "http"
		}
		cookieURL := &url.URL{
			Scheme: cookieScheme,
			Host:   cookie.Domain,
		}

		cookies := jar.Cookies(cookieURL)
		cookies = append(cookies, cookie)
		jar.SetCookies(cookieURL, cookies)

		lineNum++
	}
	c.Client.Jar = jar
	return nil
}

func parseCookieToLine(uDomain string, c *http.Cookie) (string, error) {
	ret := ""
	if c.HttpOnly {
		ret = httpOnlyPrefix
	}

	strBool := func(b bool) string {
		return strings.ToUpper(strconv.FormatBool(b))
	}

	tailMatch := true

	domain := c.Domain
	if "" == domain {
		tailMatch = false
		domain = uDomain
	}

	if !BeginWith(domain, ".") {
		domain = "." + domain
	}

	path := c.Path
	if "" == path {
		path = "/"
	}

	ret += strings.Join([]string{domain, strBool(tailMatch), path, strBool(c.Secure), strconv.FormatInt(c.Expires.Unix(), 10), c.Name, c.Value}, "\t")
	return ret, nil
}

func parseCookieLineToCookie(cookieLine string, lineNum int) (*http.Cookie, error) {
	var err error
	cookieLineHTTPOnly := false
	if strings.HasPrefix(cookieLine, httpOnlyPrefix) {
		cookieLineHTTPOnly = true
		cookieLine = strings.TrimPrefix(cookieLine, httpOnlyPrefix)
	}

	if strings.HasPrefix(cookieLine, "#") || cookieLine == "" {
		return nil, nil
	}

	cookieFields := strings.Split(cookieLine, "\t")

	if len(cookieFields) < 6 || len(cookieFields) > 7 {
		return nil, Error("incorrect number of fields in line %d.  Expected 6 or 7, got %d.", lineNum, len(cookieFields))
	}

	for i, v := range cookieFields {
		cookieFields[i] = strings.TrimSpace(v)
	}

	cookie := &http.Cookie{
		Domain:   cookieFields[0],
		Path:     cookieFields[2],
		Name:     cookieFields[5],
		HttpOnly: cookieLineHTTPOnly,
	}
	cookie.Secure, err = strconv.ParseBool(cookieFields[3])
	if err != nil {
		return nil, err
	}
	expiresInt, err := strconv.ParseInt(cookieFields[4], 10, 64)
	if err != nil {
		return nil, err
	}
	if expiresInt > 0 {
		cookie.Expires = time.Unix(expiresInt, 0)
	}

	if len(cookieFields) == 7 {
		cookie.Value = cookieFields[6]
	}

	return cookie, nil
}
